package com.tangsm.spring.boot.csv.utli;

import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
import org.apache.commons.io.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.nio.file.Files;
import java.util.Properties;
import java.util.Vector;

/**
 * sftp连接工具类
 *
 * @author tangyb
 * @see <a href="http://www.jcraft.com/jsch/">Jsch 官网</a>
 * @see <a href="https://www.cnblogs.com/longyg/archive/2012/06/25/2556576.html">JSch - Java实现的SFTP</a>
 */
public class SftpUtils {
    private static final Logger log = LoggerFactory.getLogger(SftpUtils.class);

    /**
     * sftp-SSH文件传输协议渠道
     */
    private static final String CHANNEL_SFTP = "sftp";

    /**
     * exec-执行命令渠道
     */
    private static final String CHANNEL_EXEC = "exec";

    /**
     * session会话
     */
    private Session session;
    /**
     * FTP 登录用户名
     */
    private String userName;
    /**
     * FTP 登录密码
     */
    private String password;
    /**
     * 私钥
     */
    private String privateKey;
    /**
     * FTP 服务器地址
     */
    private String host;
    /**
     * FTP 端口
     */
    private int port;

    /**
     * 私有构造
     */
    private SftpUtils() {
    }

    /**
     * 构造基于密码认证的sftp对象
     *
     * @param userName 登录用户名
     * @param password 登录密码
     * @param host     服务器地址
     * @param port     端口
     */
    public SftpUtils(String userName, String password, String host, int port) {
        this.userName = userName;
        this.password = password;
        this.host = host;
        this.port = port;
    }

    /**
     * 构造基于秘钥认证的sftp对象
     *
     * @param userName   登录用户名
     * @param host       服务器地址
     * @param port       端口
     * @param privateKey 私钥
     */
    public SftpUtils(String userName, String host, int port, String privateKey) {
        this.userName = userName;
        this.host = host;
        this.port = port;
        this.privateKey = privateKey;
    }

    /**
     * 连接sftp服务器
     */
    public void login() {
        try {
            JSch jsch = new JSch();

            // 私钥登录
            if (privateKey != null) {
                jsch.addIdentity(privateKey);
                log.info("sftp连接，私钥文件路径：{}", privateKey);
            }
            log.info("sftp 通过主机连接:{}，用户名:{}", host, userName);

            session = jsch.getSession(userName, host, port);
            log.info("会话正在构建");

            // 密码登录
            if (password != null) {
                session.setPassword(password);
            }

            // 设置持久属性配置
            Properties config = new Properties();
            // 关闭严格的主机密钥检查
            config.put("StrictHostKeyChecking", "no");
            session.setConfig(config);

            // SSH会话连接
            session.connect();
            log.info("SSH会话已连接");
        } catch (JSchException e) {
            log.error("无法连接到指定的sftp服务器，主机：{} 端口：{} USERNAME：{}", host, port, userName, e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 登出（关闭session连接）
     */
    public void logout() {
        // 调用session的关闭连接的方法
        if (session != null) {
            // session已连接，关闭连接
            if (session.isConnected()) {
                session.disconnect();
                log.info("SSH会话已经关闭");
            }
        }
    }

    /**
     * 上传文件
     *
     * @param directory  上传文件的目录
     * @param uploadFile 将要上传的文件
     */
    public void upload(String directory, String uploadFile) {
        File file = new File(uploadFile);
        try {
            upload(directory, file.getName(), Files.newInputStream(file.toPath()));
        } catch (IOException e) {
            log.error("IO异常", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 将byte[]上传到sftp，作为文件。注意:从String生成byte[]是，要指定字符集。
     *
     * @param directory    上传到sftp目录
     * @param sftpFileName 文件在sftp端的命名
     * @param byteArr      要上传的字节数组
     */
    public void upload(String directory, String sftpFileName, byte[] byteArr) {
        upload(directory, sftpFileName, new ByteArrayInputStream(byteArr));
    }

    /**
     * 将字符串按照指定的字符编码上传到sftp
     *
     * @param directory    上传到sftp目录
     * @param sftpFileName 文件在sftp端的命名
     * @param dataStr      待上传的数据
     * @param charsetName  sftp上的文件，按该字符编码保存
     */
    public void upload(String directory, String sftpFileName, String dataStr, String charsetName) {
        try {
            upload(directory, sftpFileName, new ByteArrayInputStream(dataStr.getBytes(charsetName)));
        } catch (UnsupportedEncodingException e) {
            log.error("sftp上传文件失败，不支持字符编码", e);
            throw new RuntimeException(e);
        }
    }

    /**
     * 将输入流的数据上传到sftp作为文件
     *
     * @param directory    上传到该目录
     * @param sftpFileName sftp端文件名
     * @param input        输入流
     */
    public void upload(String directory, String sftpFileName, InputStream input) {
        ChannelSftp sftp = null;
        try {
            sftp = sftpConnect();

            // 切换目录
            if(sftpFileName != null && !"".equals(sftpFileName)) {
                cd(sftp, directory);
            }

            // 上传（默认的传输模式:OVERWRITE）
            sftp.put(input, sftpFileName, ChannelSftp.OVERWRITE);

            log.info("文件：{}，sftp上传成功", sftpFileName);
        } catch (JSchException | SftpException e) {
            log.error("文件：{}，sftp上传文件失败", sftpFileName, e);
            throw new RuntimeException(e);
        } finally {
            // 关闭sftp连接
            if(sftp != null) {
                sftp.disconnect();
            }
        }
    }

    /**
     * 下载文件
     *
     * @param directory    下载目录
     * @param downloadFile 下载的文件名
     * @return 字节数组
     */
    public byte[] download(String directory, String downloadFile) {
        ChannelSftp sftp = null;
        try {
            sftp = sftpConnect();
            if (directory != null && !"".equals(directory)) {
                sftp.cd(directory);
            }
            InputStream is = sftp.get(downloadFile);

            byte[] fileData = IOUtils.toByteArray(is);

            log.info("文件：{}，sftp下载成功", downloadFile);
            return fileData;
        } catch (JSchException | SftpException | IOException e) {
            log.error("文件：{}，sftp下载失败", downloadFile, e);
            throw new RuntimeException(e);
        } finally {
            // 关闭sftp连接
            if (sftp != null) {
                sftp.disconnect();
            }
        }
    }

    /**
     * 下载文件
     *
     * @param directory    下载目录
     * @param downloadFile 下载的文件
     * @param saveFile     存在本地的路径
     */
    public void download(String directory, String downloadFile, String saveFile) {
        ChannelSftp sftp = null;
        try {
            sftp = sftpConnect();
            if (directory != null && !"".equals(directory)) {
                sftp.cd(directory);
            }

            File file = new File(saveFile);

            // 自动创建目录
            File parent = file.getParentFile();
            if (parent != null) {
                log.info("目录不存在，创建目录");
                parent.mkdirs();
            }

            sftp.get(downloadFile, Files.newOutputStream(file.toPath()));

            log.info("文件：{}，sftp下载成功", downloadFile);
        } catch (JSchException | SftpException | IOException e) {
            log.error("文件：{}，sftp下载失败", downloadFile, e);

            // 删除空文件
            File file = new File(saveFile);
            if(file.exists()) {
                file.delete();
            }

            throw new RuntimeException(e);
        } finally {
            // 关闭sftp连接
            if (sftp != null) {
                sftp.disconnect();
            }
        }
    }

    /**
     * 删除文件
     *
     * @param directory  要删除文件所在目录
     * @param deleteFile 要删除的文件
     */
    public void delete(String directory, String deleteFile){
        ChannelSftp sftp = null;
        try {
            sftp = sftpConnect();
            sftp.cd(directory);
            sftp.rm(deleteFile);
        } catch (JSchException | SftpException e) {
            log.error("删除文件失败，系统异常", e);
            throw new RuntimeException(e);
        } finally {
            // 关闭sftp连接
            if (sftp != null) {
                sftp.disconnect();
            }
        }
    }

    /**
     * 列出目录下的文件
     *
     * @param directory 要列出的目录
     * @return Vector集合
     */
    public Vector<?> listFiles(String directory) {
        ChannelSftp sftp = null;
        try {
            sftp = sftpConnect();
            return sftp.ls(directory);
        } catch (JSchException | SftpException e) {
            log.error("列出目录下的文件失败，系统异常", e);
            throw new RuntimeException(e);
        } finally {
            // 关闭sftp连接
            if (sftp != null) {
                sftp.disconnect();
            }
        }
    }

    /**
     * 执行相关的命令
     *
     * @param command 命令
     */
    public void execCommand(String command) {
        // 输入流(读)
        InputStream in = null;
        // 定义channel变量
        Channel channel = null;
        try {
            // 如果命令command不等于null
            if (command != null) {
                // 打开channel
                //说明：exec用于执行命令;sftp用于文件处理
                channel = session.openChannel(CHANNEL_EXEC);
                // 设置command
                ((ChannelExec) channel).setCommand(command);
                // channel进行连接
                channel.connect();
                // 获取到输入流
                in = channel.getInputStream();
                // 执行相关的命令
                String processDataStream = processDataStream(in);
                log.info("命令:{}", processDataStream);
            }
        } catch (Exception e) {
            log.error("执行相关的命令异常", e);
            throw new RuntimeException(e);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (channel != null) {
                channel.disconnect();
            }
        }
    }

    /**
     * 对将要执行的linux的命令进行遍历
     *
     * @param in 字节输入流
     * @return linux的命令
     * @throws IOException IO异常信息
     */
    public String processDataStream(InputStream in) throws IOException {
        StringBuilder sb = new StringBuilder();
        try (BufferedReader br = new BufferedReader(new InputStreamReader(in))) {
            String result;
            while ((result = br.readLine()) != null) {
                sb.append(result);
            }
        } catch (IOException e) {
            throw new IOException("获取数据流失败：" + e.getMessage(), e);
        }
        return sb.toString();
    }

    /**
     * 将DOS/Windows格式的路径转换为UNIX/Linux格式的路径
     *
     * @param filePath 文件路径
     * @return UNIX/Linux格式的文件路径
     */
    public static String formatSeparator(String filePath) {
        return filePath.replace("\\\\", "/");
    }

    /**
     * sftp远程连接
     * @return sftp通道
     * @throws JSchException JSch异常
     */
    private ChannelSftp sftpConnect() throws JSchException {
        if(session == null) {
            throw new JSchException("Session连接失败");
        }

        // 打开channelSftp
        ChannelSftp sftp = (ChannelSftp) session.openChannel(CHANNEL_SFTP);
        // 远程连接
        sftp.connect();

        return sftp;
    }

    /**
     * 切换目录，目录不存在创建目录
     *
     * @param sftp      sftp通道
     * @param directory 目录
     */
    private void cd(ChannelSftp sftp, String directory) throws SftpException {
        // 将DOS/Windows格式的路径转换为UNIX/Linux格式的路径
        directory = formatSeparator(directory);

        // 依次切换目录，目录不存在创建目录
        String[] folders = directory.split("/");
        for (String folder : folders) {
            if (folder.length() > 0) {
                try {
                    sftp.cd(folder);
                } catch (SftpException e) {
                    log.info("目录[{}]，不存在，创建目录", folder);
                    sftp.mkdir(folder);
                    sftp.cd(folder);
                }
            }
        }
    }
}
